// SPDX-License-Identifier: BSD-3-Clause
//
// Copyright(c) 2018 Intel Corporation. All rights reserved.
//
// Author: Bartosz Kokoszko	<bartoszx.kokoszko@linux.intel.com>
//	   Artur Kloniecki	<arturx.kloniecki@linux.intel.com>

#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <limits.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>
#include <fcntl.h>
#include <stdbool.h>
#include <termios.h>
#include "convert.h"
#include "misc.h"

#define APP_NAME "sof-logger"

#ifndef ARRAY_SIZE
#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
#endif

static const char *debugfs[] = {
	"dmac0", "dmac1", "ssp0", "ssp1",
	"ssp2", "iram", "dram", "shim",
	"mbox", "etrace",
	"hda", "pp", "dsp",
};

static void usage(void)
{
	fprintf(stdout, "Usage %s <option(s)> <file(s)>\n", APP_NAME);
	fprintf(stdout, "%s:\t \t\t\tDisplay mailbox contents\n", APP_NAME);
	fprintf(stdout, "%s:\t -i infile -o outfile\tDump infile contents "
		"to outfile\n", APP_NAME);
	fprintf(stdout, "%s:\t -l *.ldc_file\t\t*.ldc files generated by smex\n",
		APP_NAME);
	fprintf(stdout, "%s:\t -p \t\t\tInput from stdin\n", APP_NAME);
	fprintf(stdout, "%s:\t -v ver_file\t\tEnable checking firmware version "
		"with ver_file file\n", APP_NAME);
	fprintf(stdout, "%s:\t -n\t\t\tDisable checking firmware version\n",
		APP_NAME);
	fprintf(stdout, "%s:\t -c clock\t\tSet timestamp clock in MHz\n",
		APP_NAME);
	fprintf(stdout, "%s:\t -s state_name\t\tTake a snapshot of state\n",
		APP_NAME);
	fprintf(stdout, "%s:\t -t\t\t\tDisplay trace data\n", APP_NAME);
	fprintf(stdout, "%s:\t -u baud\t\tInput data from a UART\n", APP_NAME);
	fprintf(stdout, "%s:\t -r\t\t\tLess formatted output for "
		"chained log processors\n",
		APP_NAME);
	fprintf(stdout, "%s:\t -L\t\t\tHide log location in source code\n",
		APP_NAME);
	fprintf(stdout,
		"%s:\t -e 0/1\t\t\tTimestamps relative to first entry seen. Defaults to\n",
		APP_NAME);
	fprintf(stdout, "%s:\t\t\t\tabsolute when -r(aw), relative otherwise.\n",
		APP_NAME);
	fprintf(stdout, "%s:\t -f precision\t\tSet timestamp precision\n",
		APP_NAME);
	fprintf(stdout, "%s:\t -g\t\t\tHide timestamp\n",
		APP_NAME);
	fprintf(stdout, "%s:\t -d *.ldc_file \t\tDump ldc_file information\n",
		APP_NAME);
	fprintf(stdout, "%s:\t -F filter\t\tUpdate trace filter, format: "
		"<level>=<comp1>[, <comp2>]\n",
		APP_NAME);
	exit(0);
}

static int snapshot(const char *name)
{
	const char *path = "/sys/kernel/debug/sof";
	uint32_t val, addr;
	char pinname[64], poutname[64], buffer[128];
	FILE *in_fd, *out_fd;
	int i, count;

	if (!name) {
		fprintf(stderr, "error: need snapshot name\n");
		return -EINVAL;
	}

	for (i = 0; i < ARRAY_SIZE(debugfs); i++) {

		sprintf(pinname, "%s/%s", path, debugfs[i]);
		sprintf(poutname, "%s.%s.txt", name, debugfs[i]);

		/* open debugfs for reading */
		in_fd = fopen(pinname, "rb");
		if (!in_fd) {
			fprintf(stderr, "error: unable to open %s for reading: %s\n",
				pinname, strerror(errno));
			continue;
		}

		/* open outfile for writing */
		out_fd = fopen(poutname, "wb");
		if (!out_fd) {
			fprintf(stderr, "error: unable to open %s for writing: %s\n",
				poutname, strerror(errno));
			fclose(in_fd);
			continue;
		}

		fprintf(stdout, "processing %s...\n", pinname);
		addr = 0;

		while (1) {
			count = fread(&val, 1, 4, in_fd);
			if (count != 4)
				break;

			sprintf(buffer, "0x%6.6x: 0x%8.8x\n", addr, val);

			count = fwrite(buffer, 1, strlen(buffer), out_fd);

			addr += 4;
		}

		fclose(in_fd);
		fclose(out_fd);
	}

	return 0;
}

static int configure_uart(const char *file, unsigned int baud)
{
	struct termios tio = {};
	int ret, fd = open(file, O_RDWR | O_NOCTTY);
	if (fd < 0)
		return -errno;

	cfsetspeed(&tio, 115200);
	cfmakeraw(&tio);

	tio.c_iflag |= IGNBRK;

	tio.c_cflag |= CLOCAL | CREAD | HUPCL;

	tio.c_cc[VTIME] = 1;
	tio.c_cc[VMIN] = 1;

	ret = tcsetattr(fd, TCSANOW, &tio);
	return ret < 0 ? -errno : fd;
}

/* Concantenate `config->filter_config` with `input` + `\n` */
static int append_filter_config(struct convert_config *config, const char *input)
{
	char *old_config = config->filter_config;

	/* filer_config can't be NULL for following steps */
	if (!old_config)
		old_config = asprintf("%s", "");

	config->filter_config = asprintf("%s%s\n", old_config, input);
	free(old_config);
	if (!config->filter_config)
		return -ENOMEM;
	return 0;
}

int main(int argc, char *argv[])
{
	static const char optstring[] = "ho:i:l:ps:c:u:tv:rd:Le:f:gF:n";
	struct convert_config config;
	unsigned int baud = 0;
	const char *snapshot_file = 0;
	int opt, ret = 0;

	config.trace = 0;
	config.clock = 19.2;
	config.in_file = NULL;
	config.out_file = NULL;
	config.out_fd = NULL;
	config.in_fd = NULL;
	config.ldc_file = NULL;
	config.ldc_fd = NULL;
	config.input_std = 0;
	/* checking fw version is disabled by default */
	config.version_file = "/sys/kernel/debug/sof/fw_version";
	config.version_fd = NULL;
	config.version_fw = 1;
	config.use_colors = 1;
	config.serial_fd = -EINVAL;
	config.raw_output = 0;
	config.dump_ldc = 0;
	config.hide_location = 0;
	config.time_precision = 6;
	config.relative_timestamps = INT_MAX; /* unspecified */
	config.filter_config = NULL;

	while ((opt = getopt(argc, argv, optstring)) != -1) {
		switch (opt) {
		case 'o':
			config.out_file = optarg;
			break;
		case 'i':
			config.in_file = optarg;
			break;
		case 't':
			config.trace = 1;
			break;
		case 'c':
			config.clock = atof(optarg);
			break;
		case 's':
			snapshot_file = optarg;
			break;
		case 'l':
			if (config.ldc_file) {
				fprintf(stderr, "error: Multiple ldc files\n");
				usage();
			}
			config.ldc_file = optarg;
			break;
		case 'p':
			config.input_std = 1;
			break;
		case 'u':
			baud = atoi(optarg);
			break;
		case 'r':
			config.raw_output = 1;
			break;
		case 'v':
			/* enabling checking fw version with ver_file file */
			config.version_file = optarg;
			break;
		case 'n':
			config.version_fw = 0;
			break;
		case 'L':
			config.hide_location = 1;
			break;
		case 'e': {
			int i = atoi(optarg);

			if (i < 0 || 1 < i) {
				fprintf(stderr, "%s: invalid option: -e %s\n",
					APP_NAME, optarg);
				return -EINVAL;
			}
			config.relative_timestamps = i;
		}
		case 'f':
			config.time_precision = atoi(optarg);
			if (config.time_precision < 0) {
				usage();
				return -EINVAL;
			}
			break;
		case 'g':
			config.time_precision = -1;
			break;
		case 'd':
			if (config.ldc_file) {
				fprintf(stderr, "error: Multiple ldc files\n");
				usage();
			}
			config.dump_ldc = 1;
			config.version_fw = 0;
			config.ldc_file = optarg;
			break;
		case 'F':
			ret = append_filter_config(&config, optarg);
			if (ret < 0)
				return ret;
			break;
		case 'h':
		default: /* '?' */
			usage();
		}
	}

	if (argc != optind) {
		fprintf(stderr, "error: Unused parameter '%s'\n", argv[optind]);
		usage();
	}

	if (snapshot_file)
		return baud ? EINVAL : -snapshot(snapshot_file);

	if (!config.ldc_file) {
		fprintf(stderr, "error: Missing ldc file\n");
		usage();
	}

	config.ldc_fd = fopen(config.ldc_file, "rb");
	if (!config.ldc_fd) {
		ret = errno;
		fprintf(stderr, "error: Unable to open ldc file %s: %s\n",
			config.ldc_file, strerror(ret));
		goto out;
	}

	if (config.version_fw) {
		config.version_fd = fopen(config.version_file, "rb");
		if (!config.version_fd) {
			ret = errno;
			fprintf(stderr,
				"error: Unable to open version file %s: %s\n",
				config.version_file, strerror(ret));
			goto out;
		}
	}

	if (config.out_file) {
		config.out_fd = fopen(config.out_file, "w");
		if (!config.out_fd) {
			ret = errno;
			fprintf(stderr, "error: Unable to open out file %s: %s\n",
				config.out_file, strerror(ret));
			goto out;
		}
	} else {
		config.out_fd = stdout;
	}

	/* trace requested ? */
	if (config.trace)
		config.in_file = "/sys/kernel/debug/sof/trace";

	/* Default value when -e is not specified */
	if (config.relative_timestamps == INT_MAX)
		config.relative_timestamps = config.raw_output ? 0 : 1;

	/* default option with no infile is to dump errors/debug data */
	if (!config.in_file && !config.dump_ldc)
		config.in_file = "/sys/kernel/debug/sof/etrace";

	if (config.input_std) {
		config.in_fd = stdin;
	} else if (baud) {
		config.serial_fd = configure_uart(config.in_file, baud);
		if (config.serial_fd < 0) {
			ret = -config.serial_fd;
			goto out;
		}
	} else if (config.in_file) {
		config.in_fd = fopen(config.in_file, "rb");
		if (!config.in_fd) {
			ret = errno;
			fprintf(stderr,
				"error: Unable to open in file %s: %s\n",
				config.in_file, strerror(ret));
			goto out;
		}
	}
	if (isatty(fileno(config.out_fd)) != 1)
		config.use_colors = 0;

	ret = -convert(&config);

out:
	/* free memory */
	if (config.filter_config)
		free(config.filter_config);

	/* close files */
	if (config.out_fd)
		fclose(config.out_fd);

	if (config.in_fd)
		fclose(config.in_fd);

	if (config.ldc_fd)
		fclose(config.ldc_fd);

	if (config.version_fd)
		fclose(config.version_fd);

	return ret;
}
